######################################## 文件 IO ######################################## 缓冲类型 **************************************** 标准 I/O 库中有三种缓冲概念: +----------+-------------------------------------+ | 缓冲类型 | 行为 | +==========+=====================================+ | 全缓冲 | 当缓冲区满时才执行 I/O 操作 | +----------+-------------------------------------+ | 行缓冲 | 遇到换行符或缓冲区满时执行 I/O 操作 | +----------+-------------------------------------+ | 无缓冲 | 立即执行 I/O 操作 | +----------+-------------------------------------+ 使用 flush 函数可以立即刷新流,其具有以下特点: - 终端驱动程序中的 flush 表示丢弃缓冲区的内容 - 当尝试从一个不带缓冲或行缓冲的流中输入数据时,会刷新所有 **行缓冲** 流 ISO C 做出了对缓冲特征的部分规定: - 当且仅当 stdin 和 stdout 不指向交互式设备时,它们才是全缓冲的 - stderr 绝对不会是全缓冲的 下面的特征是实现定义的: - stderr 是无缓冲的 - 指向终端的 stdin 和 stdout 是行缓冲的,否则为全缓冲 在多线程中缓冲可以会带来一些问题,以下面的代码为例: .. code-block:: c #include #include #include int main() { printf("hello, world\n"); fork(); return 0; } 在终端中直接执行的时候输出为一行内容,重定向到文件后输出为两行。原因是 stdio 默认是行缓冲的,而重定向到文件后,fork 会把父进程缓冲区的内容也复制一份,在进程终止时被强制执行了缓冲区刷新 记录锁 **************************************** 记录锁允许进程对文件的一部分进行锁定,锁定的方式是读写锁。但是与读写锁不同的是,读写锁是用于线程同步的,加锁后如果想再加锁必须先解除以前的锁,而记录锁后面加的锁会解除前面的锁,另外 - 进程间的记录锁遵循读写锁的共享互斥原则 - 单一进程的新锁会覆盖旧锁 - 在文件上加锁时,系统对加锁区域根据条件进行合并或者分裂 记录锁的 API 如下: .. code-block:: c int fcntl(int fd, int cmd, flock* flockptr); 参数说明如下: flock 是一个结构体,其定义为: .. code-block:: c struct flock{ short l_type; short l_whence; off_t l_start; off_t l_len; pid_t l_pid; }; 数据成员描述如下: +-------------------------+----------+------------------------------------------------+ | 成员名 | 取值 | 说明 | +=========================+==========+================================================+ | | F_RDLCK | 读锁 | | +----------+------------------------------------------------+ | l_type | F_WRLCK | 写锁 | | +----------+------------------------------------------------+ | | F_UNLCK | 解锁 | +-------------------------+----------+------------------------------------------------+ | | SEEK_SET | | | +----------+------------------------------------------------+ | l_whence | SEEK_CUR | 当前位置 | | +----------+------------------------------------------------+ | | SEEK_END | 文件末尾 | +-------------------------+----------+------------------------------------------------+ | l_start | | 指针偏移量 | +-------------------------+----------+------------------------------------------------+ | l_len | | 要锁定区域的长度 | +-------------------------+----------+------------------------------------------------+ | l_pid | | 进程 PID 为 l_pid 的进程持有的锁能阻塞当前进程 | +-------------------------+----------+------------------------------------------------+ 其中 l_pid 是一个输出变量,当 cmd = F_GETLK 时得到此变量,l_pid 代表了持有锁的进程(也是导致当前进程无法获得锁的进程) 当 l_len == 0 时,则表示从 l_whence + l_start 之后的所有区域,因此锁定一个文件的方法是另 l_whence = SEEK_SET, l_start = 0, l_len = 0 另外,锁可以越过文件末尾,但是无法超过开头(因为文件允许有空洞) .. important:: 在使用 SEEK_END 时要额外注意,因为 SEEK_END 指向的始终是文件的末尾,你每次修改数据文件的末尾都会发生变化,相应地,文件指针的偏移量 l_start 也一直在变化 参数 cmd 的取值为: +--------------------------+---------------------------------------------------------------------------------------------------------------------+ | 取值 | 说明 | +==========================+=====================================================================================================================+ | F_GETLK | 根据 flockptr 测试是否可以加锁,若可以加锁,则将 flockptr->l_type 设置为 F_UNLCK,否则将已有锁的信息复制到 flockptr | +--------------------------+---------------------------------------------------------------------------------------------------------------------+ | F_SETLK | 根据 flockptr 尝试得到锁,若失败,则 fcntl 立即返回,并设 error 为 EAGAIN | +--------------------------+---------------------------------------------------------------------------------------------------------------------+ | F_SETLK :abbr:`W (wait)` | 根据 flockptr 尝试得到锁,若无法得到锁则阻塞至成功。若 flockptr->l_type == F_UNLCK,则表示清除 flockptr 代表的锁 | +--------------------------+---------------------------------------------------------------------------------------------------------------------+ .. note:: 实际上 F_SETLK 的错误返回在规范中允许 error == EAGAIN 或 error == EACCESS,但是在实现中,总是领 error == EAGAIN 另外,记录锁还有一些其它性质: - 锁的生命周期取进程和文件生命周期的较小者。也就是说如果不手动释放锁,那么锁要么在文件关闭时被释放,要么在进程关闭时被释放 - 子进程不继承父进程的锁 - 在执行 exec 后,新程序继承原程序的锁 建议锁和强迫锁 ======================================== 如果我们的进程全部通过数据库进程访问数据,那么我们只需要在数据库进程上加锁就行了,但是外部程序可以自由地修改数据,这种锁称为建议锁 如果锁是由操作系统保证的,用户加完锁后其它用户的任意进程无法修改数据,就说这个锁是强迫锁 尽管操作系统会对文件加锁,但是不会对 unlink 进行限制,因此可以通过删除原有文件再创建新文件的方式绕过强迫锁 如果多个进程同时对一个不加锁的文件进行操作,那么文件的内容取决于最后退出的程序 文件锁 **************************************** 相比记录锁而言,文件锁更加简单易用: .. code-block:: c int lockf(int fd, int cmd, off_t len); int flock(int fd, int operation); flock 和 lockf 不同之处在于 flock 是一个建议锁。对于 lockf 而言,fd 指明了需要操作的文件描述符,cmd 指明了操作,而 len 指明了从当前文件指针开始锁定的字节数量 cmd 可为: +---------+-----------------------------------------------------------------------------------------------------------------------------------------+ | 参数 | 说明 | +=========+=========================================================================================================================================+ | F_LOCK | 为文件的一段加锁 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------+ | F_TLOCK | 尝试加锁 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------+ | F_ULOCK | 解锁 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------+ | F_TEST | 测试锁,如果文件中被测试的部分没有锁定或者是调用进程持有锁就返回 0;如果是其它进程持有锁就返回 -1,并且 errno 设置为 EAGAIN 或 EACCES。 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------+ .. tip:: 文件锁一个可用的操作是进程之间通过创建锁文件来相互通信。在包管理器中经常会有这个用法